package com.cocotingo.snail.template;

import java.awt.Color;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.LinkedList;

import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.cocotingo.snail.GraphicsX;
import com.cocotingo.snail.template.layout.Layout;
import com.cocotingo.snail.template.layout.LayoutException;
import com.cocotingo.snail.template.layout.LayoutFactor;
import com.cocotingo.snail.template.resource.AcceleratedImageResource;
import com.cocotingo.snail.template.resource.Resource;
import com.cocotingo.snail.template.resource.StretchableImageResource;

public class Template {
	
	private final TemplateLoader loader;
	private final String id;
	private final Node root;
	
	Template(TemplateLoader loader, String id, Node root) {
		this.loader = loader;
		this.id = id;
		this.root = root;
	}

	public String getID() {
		return id;
	}
	
	private static class HandlerCaller {
		
		private final SealedView target;
		private final Hashtable<String, Method> handlers;
		
		private HandlerCaller(SealedView target) {
			this.target = target;
			handlers = new Hashtable<String, Method>();
		}
		
		private void registerHandler(String handlerName) throws TemplateException {
			if (!handlers.containsKey(handlerName)) {
				try {
					Method handler = target.getClass().getMethod(handlerName, SealedView.class, Object.class);
					if (!handler.isAnnotationPresent(TemplateCallback.class))
						throw new TemplateException("missing TemplateCallback annotation on handler: " + handlerName);
					handlers.put(handlerName, handler);
				} catch (SecurityException e) {
					throw new TemplateException("missing handler: " + handlerName, e);
				} catch (NoSuchMethodException e) {
					throw new TemplateException("missing handler: " + handlerName, e);
				}
			}
		}
		
		private void callHandler(String handlerName, SealedView sender, Object arg) {
			Method handler = handlers.get(handlerName);
			if (handler != null) {
				try {
					handler.invoke(target, sender, arg);
				} catch (IllegalArgumentException e) {
				} catch (IllegalAccessException e) {
				} catch (InvocationTargetException e) {
				}
			}
		}
		
	}
	
	private static class ValueGetter {
		
		private SealedView target;
		private final Hashtable<String, Method> getters;
		
		private ValueGetter() {
			getters = new Hashtable<String, Method>();
		}
		
		private void setTarget(SealedView target) {
			this.target = target;
		}
		
		private void registerGetter(String getterName) throws TemplateException {
			if (!getters.containsKey(getterName)) {
				try {
					Method handler = target.getClass().getMethod(getterName);
					if (!handler.isAnnotationPresent(TemplateCallback.class))
						throw new TemplateException("missing TemplateCallback annotation on handler: " + getterName);
					getters.put(getterName, handler);
				} catch (SecurityException e) {
					throw new TemplateException("missing getter: " + getterName, e);
				} catch (NoSuchMethodException e) {
					throw new TemplateException("missing getter: " + getterName, e);
				}
			}
		}
		
		private Object callGetter(String getterName) {
			Method getter = getters.get(getterName);
			if (getter != null) {
				try {
					return getter.invoke(target);
				} catch (IllegalArgumentException e) {
				} catch (IllegalAccessException e) {
				} catch (InvocationTargetException e) {
				}
			}
			return null;
		}
		
	}
	
	public SealedView createView() throws TemplateException {
		NodeList nodes = root.getChildNodes();
		// find root view node
		Node rootViewNode = null;
		LinkedList<Node> layoutNodes = new LinkedList<Node>();
		for (int i = 0; i < nodes.getLength(); i++) {
			Node node = nodes.item(i);
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				if (node.getNodeName().equals("View")) {
					if (rootViewNode == null) rootViewNode = node;
					else throw new TemplateException("duplicate root view node in Template: " + id);
				} else if (node.getNodeName().equals("Layout")) {
					layoutNodes.add(node);
				} else {
					throw new TemplateException("unknown element in view node: " + node.getNodeName());
				}
			}
		}
		if (rootViewNode == null) throw new TemplateException("missing root View node in template: " + id);
		final Hashtable<String, SealedView> nameMap = new Hashtable<String, SealedView>();
		final ValueGetter getter = new ValueGetter();
		SealedView rootView = createViewWithViewNode(rootViewNode, null, nameMap, null, getter);
		rootView.setNameTable(nameMap);
		for (Node node : layoutNodes) {
			setViewLayout(node, rootView, getter, nameMap);
		}
		return rootView;
	}
	
	private SealedView createViewWithViewNode(Node node, SealedView owner, Hashtable<String, SealedView> nameMap, HandlerCaller caller, ValueGetter getter) throws TemplateException {
		// create view object via node
		final NamedNodeMap map = node.getAttributes();
		final NodeList children = node.getChildNodes();
		final SealedView view = createViewObject(map);
		if (caller == null) { // root view
			caller = new HandlerCaller(view);
			getter.setTarget(view);
			nameMap.putAll(view.getNameTable());
		}
		
		// add view name
		Node nameNode = map.getNamedItem("name");
		if (nameNode != null) {
			String name = nameNode.getNodeValue();
			if (!nameMap.containsKey(name)) {
				nameMap.put(name, view);
			} else {
				throw new TemplateException("duplicate name in view node:" + name);
			}
		}
		
		// set view properties
		for (int i = 0; i < map.getLength(); i++) {
			Node attr = map.item(i);
			if (!attr.getNodeName().equals("template") &&
				!attr.getNodeName().equals("class") &&
				!attr.getNodeName().equals("name")) {
				setViewProperty(view, attr.getNodeName(), attr.getNodeValue(), getter);
			}
		}
		
		// deal with children
		boolean hasPaintNode = false;
		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE) {
				if (child.getNodeName().equals("View")) {
					createViewWithViewNode(child, view, nameMap, caller, getter);
				} else if (child.getNodeName().equals("Handler")) {
					registerHandler(child, view, caller);
				} else if (child.getNodeName().equals("Paint")) {
					if (!hasPaintNode) {
						setViewPainter(child, view);
						hasPaintNode = true;
					} else {
						throw new TemplateException("duplicate paint node");
					}
				} else {
					throw new TemplateException("unknown element in view node: " + child.getNodeName());
				}
			}
		}
		
		// add view to owner
		if (owner != null) {
			owner.addSubview(view);
		}
		
		return view;
	}
	
	private SealedView createViewObject(NamedNodeMap map) throws TemplateException {
		Node templateNode = map.getNamedItem("template");
		Node classNode = map.getNamedItem("class");
		if (templateNode != null && classNode == null) {
			String templateID = templateNode.getNodeValue();
			if (templateID.equals(id)) throw new TemplateException("can not reference template in itself: " + templateID);
			Template template = loader.getTemplate(templateID);
			if (template != null) {
				return template.createView();
			} else {
				throw new TemplateException("missing template id: " + templateID);
			}
		} else if (classNode != null && templateNode == null) {
			String className = classNode.getNodeValue();
			try {
				Class<?> cls = Class.forName(className);
				Object obj = cls.newInstance();
				if (obj instanceof SealedView) {
					return (SealedView) obj;
				} else {
					throw new TemplateException("class " + className + " is not a subclass of SealedView");
				}
			} catch (ClassNotFoundException e) {
				throw new TemplateException("missing class name: " + className, e);
			} catch (InstantiationException e) {
				throw new TemplateException("can not create instance of class: " + className, e);
			} catch (IllegalAccessException e) {
				throw new TemplateException("can not create instance of class: " + className, e);
			}
		} else if (templateNode == null && classNode == null) {
			return new SealedView();
		} else {
			throw new TemplateException("class attribute and template attribute can not be used together in a single view node");
		}
	}
	
	private void setViewProperty(SealedView view, String name, String value, ValueGetter getter) throws TemplateException {
		/*
			int left, top, width, height;
			int tag;
			float alpha;
			boolean clipped;
			boolean hidden;
			boolean paintingEnable;
			boolean keyboardFocusable;
			Color backgroundColor;
		*/
		if (name.equals("left")) view.setLeft(getIntValue(value, getter));
		else if (name.equals("top")) view.setTop(getIntValue(value, getter));
		else if (name.equals("width")) view.setWidth(getIntValue(value, getter));
		else if (name.equals("height")) view.setHeight(getIntValue(value, getter));
		else if (name.equals("tag")) view.setTag(getIntValue(value, getter));
		else if (name.equals("alpha")) view.setAlpha(getFloatValue(value, getter));
		else if (name.equals("clipped")) view.setClipped(getBooleanValue(value, getter));
		else if (name.equals("hidden")) view.setHidden(getBooleanValue(value, getter));
		else if (name.equals("paintingEnabled")) view.setPaintingEnabled(getBooleanValue(value, getter));
		else if (name.equals("keyboardFocusable")) view.setKeyboardFocusable(getBooleanValue(value, getter));
		else if (name.equals("backgroundColor")) view.setBackgroundColor(getColorValue(value, getter));
		else throw new TemplateException("unknown attribute: " + name);
	}
	
	private int getIntValue(String value, ValueGetter getter) throws TemplateException {
		try {
			return Integer.parseInt(value);
		} catch (NumberFormatException e) {
			// try to find callback
			try {
				getter.registerGetter(value);
				Object ret = getter.callGetter(value);
				return (Integer) ret;
			} catch (TemplateException e1) {
				throw new TemplateException("can not recognize attribute value: " + value);
			} catch (ClassCastException e2) {
				throw new TemplateException("dismatch getter return value type: " + value, e2);
			}
		}
	}
	
	private float getFloatValue(String value, ValueGetter getter) throws TemplateException {
		try {
			return Float.parseFloat(value);
		} catch (NumberFormatException e) {
			// try to find callback
			try {
				getter.registerGetter(value);
				Object ret = getter.callGetter(value);
				return (Float) ret;
			} catch (TemplateException e1) {
				throw new TemplateException("can not recognize attribute value: " + value);
			} catch (ClassCastException e2) {
				throw new TemplateException("dismatch getter return value type: " + value, e2);
			}
		}
	}
	
	private boolean getBooleanValue(String value, ValueGetter getter) throws TemplateException {
		if (value.equals("true")) return true;
		else if (value.equals("false")) return false;
		else {
			// try to find callback
			try {
				getter.registerGetter(value);
				Object ret = getter.callGetter(value);
				return (Boolean) ret;
			} catch (TemplateException e1) {
				throw new TemplateException("can not recognize attribute value: " + value);
			} catch (ClassCastException e2) {
				throw new TemplateException("dismatch getter return value type: " + value, e2);
			}
		}
	}
	
	private Color getColorValue(String value, ValueGetter getter) throws TemplateException {
		try {
			return loader.getColor(value);
		} catch (TemplateException e) {
			// try to find callback
			try {
				getter.registerGetter(value);
				Object ret = getter.callGetter(value);
				return (Color) ret;
			} catch (TemplateException e1) {
				throw new TemplateException("can not recognize attribute value: " + value);
			} catch (ClassCastException e2) {
				throw new TemplateException("dismatch getter return value type: " + value, e2);
			}
		}
	}
	
	private void registerHandler(Node handlerNode, SealedView view, final HandlerCaller caller) throws TemplateException {
		NamedNodeMap map = handlerNode.getAttributes();
		Node eventNode = map.getNamedItem("event");
		Node handlerNameNode = map.getNamedItem("handler");
		if (eventNode == null) throw new TemplateException("missing event attribute in handler node");
		if (handlerNameNode == null) throw new TemplateException("missing handler attribute in handler node");
		final String eventName = eventNode.getNodeValue();
		final String handlerName = handlerNameNode.getNodeValue();
		caller.registerHandler(handlerName);
		view.addEventHandler(new EventHandler() {
			@Override
			public void handleViewEvent(SealedView view, String event, Object arg) {
				if (eventName.equals(event)) {
					caller.callHandler(handlerName, view, arg);
				}
			}
		});
	}
	
	private void setViewLayout(Node layoutNode, SealedView rootView, ValueGetter getter, Hashtable<String, SealedView> nameMap) throws TemplateException {
		NamedNodeMap map = layoutNode.getAttributes();
		Node targetNode = map.getNamedItem("target");
		if (targetNode == null) throw new TemplateException("missing target attribute in layout node");
		final String targetName = targetNode.getNodeValue();
		final SealedView target = rootView.getNamedView(targetName);
		if (target == null) throw new TemplateException("can not find view with name: " + targetName);
		
		final Layout layout = new Layout();
		NodeList factorNodes = layoutNode.getChildNodes();
		for (int i = 0; i < factorNodes.getLength(); i++) {
			Node factorNode = factorNodes.item(i);
			if (factorNode.getNodeType() == Node.ELEMENT_NODE) {
				LayoutFactor factor = createLayoutFactor(factorNode, getter, nameMap, target);
				try {
					layout.addFactor(factor);
				} catch (LayoutException e) {
					throw new TemplateException("invalid layout factor: " + factorNode.getNodeName(), e);
				}
			}
		}
		
		rootView.addEventHandler(new EventHandler() {
			@Override
			public void handleViewEvent(SealedView view, String eventName, Object arg) {
				if (eventName.equals(SealedView.PROPERTY_CHANGED_SIZE)) {
					layout.applyToView(target);
				}
			}
		});
	}
	
	private LayoutFactor createLayoutFactor(Node factorNode, final ValueGetter getter, Hashtable<String, SealedView> nameMap, final SealedView target) throws TemplateException {
		String name = factorNode.getNodeName();
		int type = -1;
		if (name.equals("Left")) type = LayoutFactor.LEFT;
		else if (name.equals("HorizontalCenter")) type = LayoutFactor.HORIZONTAL_CENTER;
		else if (name.equals("Right")) type = LayoutFactor.RIGHT;
		else if (name.equals("Width")) type = LayoutFactor.WIDTH;
		else if (name.equals("Top")) type = LayoutFactor.TOP;
		else if (name.equals("VerticalCenter")) type = LayoutFactor.VERTICAL_CENTER;
		else if (name.equals("Bottom")) type = LayoutFactor.BOTTOM;
		else if (name.equals("Height")) type = LayoutFactor.HEIGHT;
		else throw new TemplateException("invalid layout factor name: " + name);
		
		// attributes
		NamedNodeMap map = factorNode.getAttributes();
		Node typeNode = map.getNamedItem("type");
		
		// find text node
		NodeList textNodes = factorNode.getChildNodes();
		if (textNodes.getLength() < 1) throw new TemplateException("missing value in layout factor: " + name);
		else if (textNodes.getLength() > 1) throw new TemplateException("too many values in layout factor: " + name);
		Node textNode = textNodes.item(0);
		if (textNode.getNodeType() != Node.TEXT_NODE) throw new TemplateException("missing value in layout factor: " + name);
		final String text = textNode.getTextContent();
		
		// create factor
		LayoutFactor factor = null;
		if (typeNode == null || typeNode.getNodeValue().equals("absolute")) {
			factor = new LayoutFactor(type) {
				@Override
				public int getValue() {
					try {
						return getIntValue(text, getter);
					} catch (TemplateException e) {
					}
					return 0;
				}
			};
		} else if (typeNode.getNodeValue().equals("binding")) {
			final SealedView ref = nameMap.get(text.trim());
			if (ref == null) throw new TemplateException("unknown binding view name: " + text);
			factor = new LayoutFactor(type) {
				@Override
				public int getValue() {
					switch (getFactorType()) {
					case LayoutFactor.LEFT:
						return ref.getLeft();
					case LayoutFactor.HORIZONTAL_CENTER:
						return ref.getLeft() + ref.getWidth() / 2;
					case LayoutFactor.RIGHT:
						return ref.getRight();
					case LayoutFactor.WIDTH:
						return ref.getWidth();
					case LayoutFactor.TOP:
						return ref.getTop();
					case LayoutFactor.VERTICAL_CENTER:
						return ref.getTop() + ref.getHeight() / 2;
					case LayoutFactor.BOTTOM:
						return ref.getBottom();
					case LayoutFactor.HEIGHT:
						return ref.getHeight();
					}
					return 0;
				}
			};
		} else if (typeNode.getNodeValue().equals("reference")) {
			if (type != LayoutFactor.LEFT && type != LayoutFactor.RIGHT && type != LayoutFactor.TOP && type != LayoutFactor.BOTTOM) {
				throw new TemplateException("type attribute (reference) conflicts with layout factor type: " + name);
			}
			final SealedView ref = nameMap.get(text.trim());
			if (ref == null) throw new TemplateException("unknown binding view name: " + text);
			Node marginNode = map.getNamedItem("margin");
			final int margin = marginNode != null ? getIntValue(marginNode.getNodeValue(), getter) : 0;
			factor = new LayoutFactor(type) {
				@Override
				public int getValue() {
					switch (getFactorType()) {
					case LayoutFactor.LEFT:
						return ref.getLeft() + ref.getWidth() + margin;
					case LayoutFactor.RIGHT:
						return target.getWidth() - ref.getLeft() + margin;
					case LayoutFactor.TOP:
						return ref.getTop() + ref.getHeight() + margin;
					case LayoutFactor.BOTTOM:
						return target.getHeight() - ref.getTop() + margin;
					}
					return 0;
				}
			};
		} else {
			throw new TemplateException("unknown layout factor type: " + typeNode.getNodeValue());
		}
		
		return factor;
	}
	
	private void setViewPainter(Node node, SealedView view) throws TemplateException {
		NodeList children = node.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node child = children.item(i);
			if (child.getNodeType() == Node.ELEMENT_NODE) {
				if (child.getNodeName().equals("Image")) {
					setViewImagePainter(child, view);
				} else {
					throw new TemplateException("unknown paint element: " + child.getNodeName());
				}
			}
		}
	}
	
	private void setViewImagePainter(Node node, SealedView view) throws TemplateException {
		NamedNodeMap attr = node.getAttributes();
		Node resNode = attr.getNamedItem("resource");
		if (resNode == null) throw new TemplateException("missing rsource attribute in image node");
		String resName = resNode.getNodeValue();
		final Resource res = loader.getResource(resName);
		if (res == null) throw new TemplateException("can not find resource named: " + resName);
		if (res instanceof StretchableImageResource) {
			view.addEventHandler(new EventHandler() {
				@Override
				public void handleViewEvent(SealedView view, String eventName, Object arg) {
					if (eventName.equals(SealedView.REPAINT_VIEW)) {
						GraphicsX g = (GraphicsX) arg;
						g.drawStrechableImage((StretchableImageResource) res, view.getWidth(), view.getHeight());
					}
				}
			});
		} else if (res instanceof AcceleratedImageResource) {
			view.addEventHandler(new EventHandler() {
				@Override
				public void handleViewEvent(SealedView view, String eventName, Object arg) {
					if (eventName.equals(SealedView.REPAINT_VIEW)) {
						GraphicsX g = (GraphicsX) arg;
						g.drawImage((AcceleratedImageResource) res, 0, 0, view.getWidth(), view.getHeight());
					}
				}
			});
		} else {
			throw new TemplateException("invalid resource type: " + resName);
		}
	}
	
}
